<template>
  <div class="customize-upload" v-if="concurrencyUploadState" :style="{
    display: concurrencyUploadState ? 'block' : 'none'
  }">
    <div class="customize-upload-over"></div>
    <div class="customize-upload-content">
      <div class="customize-upload-content-head">
        <label v-if="showFileUploadBtn" class="customize-upload-content-upload-btn" :style="{
            pointerEvents: concurrencyUploadClosState ? 'all' : 'none',
            backgroundColor: concurrencyUploadClosState ? '#409EFF' : '#EEEEEE',
            color: concurrencyUploadClosState ? 'white' : '#ABABAB'
          }">
          选择文件
          <input :multiple="hasMultiple" type="file" style="display: none"  @change="choiceFiles($event)"/>
        </label>
        <label v-if="showDIRUploadBtn" class="customize-upload-content-upload-btn" style="margin-left: 10px;" :style="{
             pointerEvents: concurrencyUploadClosState ? 'all' : 'none',
             backgroundColor: concurrencyUploadClosState ? '#409EFF' : '#EEEEEE',
             color: concurrencyUploadClosState ? 'white' : '#ABABAB'
          }">
          选择文件夹
          <input webkitdirectory type="file" style="display: none"  @change="choiceFiles($event)"/>
        </label>
        <label @click="uploadFileQueue" class="customize-upload-content-upload-btn" style="margin-left: 10px" :style="{
            pointerEvents: concurrencyUploadClosState ? 'all' : 'none',
             backgroundColor: concurrencyUploadClosState ? '#409EFF' : '#EEEEEE',
             color: concurrencyUploadClosState ? 'white' : '#ABABAB'
          }">
          上传
        </label>
        <label style="margin-left: 10px;font-size: 14px">
          {{notice}}
        </label>
        <label :style="{
         color: concurrencyUploadClosState ? 'black' : '#ABABAB'
        }" class="customize-upload-close" @click="closeCustomizeUpload">
          ×
        </label>
      </div>
      <div ref="processContentRef" class="customize-upload-content-process">
        <div v-for="(item, index) in uploadFiles" class="customize-upload-content-process-item" :id="'item-' + index">
          <div class="show-process" v-if="!item.hasDelete">
            <div class="process-item-all"></div>
            <div class="process-item-real" :style="{
              width: item.process + '%'
            }">{{ item.process }}%
            </div>
          </div>
          <div class="show-info">
            <div class="show-info-delete show-info-spacing" v-show="item.showDelete && concurrencyUploadClosState "
                 @click="deleteUploadItem(item.uuid)">×
            </div>
            <div class="show-info-name show-info-spacing">{{ item.name }}</div>
            <div class="show-info-reckon-size show-info-spacing">{{ item.reckonSize }}</div>
            <!--            <div class="show-info-spacing">|</div>
                        <div class="show-info-upload-size show-info-spacing" >{{item.uploadSize}}</div>-->
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>

import {computed, reactive, ref} from "vue";
import ArrayQueue from "@/components/plugs/upload/ArrayQueue.ts"
import {ElLoading, ElMessage} from "element-plus";
import axios from "axios";
import {TOKEN_NAME_KEY, TOKEN_VALUE_KEY} from "@/constant/constant.ts";
import {UploadConstants} from '@/components/plugs/upload/UploadConstants.ts'
import {ChoiceWay, type UploadView} from "@/components/plugs/upload/UploadInterface.ts";
import {isEmpty} from "lodash";

const tokenName = localStorage.getItem(TOKEN_NAME_KEY) ?? "";
const tokenValue = localStorage.getItem(TOKEN_VALUE_KEY) ?? "";
axios.defaults.withCredentials = false; // 允许携带凭证信息，如 cookies
axios.defaults.headers[tokenName] = tokenValue;

const props = withDefaults(defineProps<UploadView>(), {
  url: '/system/uploadFile',
  mergeUrl: '/system/mergeFile',
  suffix: "",
  fileSize: -1, // -1不限制
  choiceWay: ChoiceWay.FILE_DIR,
  uploadFileNum: -1,
  error: (state: UploadConstants, data: []) => {
  },
  uploadSuccess: (state: UploadConstants, data: []) => {
  },
  beforeClose: () => {
  },
  splitSize: 1024 * 1024  //10M
});
const {suffix, splitSize, url, mergeUrl, uploadSuccess, error, beforeClose, choiceWay, fileSize, uploadFileNum} = props;
const workerCount = navigator.hardwareConcurrency || 4 // 内核数量，如果取不到则为4
const speed = ref(0);
const concurrencyUploadState = ref(false);
const concurrencyUploadClosState: any = ref(true);
let commandCallBack!: any;
let loadingInstance!: any;
let concurrencyUploadNum: number = 5;
const processContentRef: any = ref(null);
const uploadFiles: any = reactive([]);
let mergeFiles: any = [];
let currentBatchFiles = 0;
let queue = new ArrayQueue();
let lastSplitFileIndex = 0;
const suffixArray = suffix.split(",");

const hasMultiple = computed(() =>{
  return uploadFileNum > 1;
})

const notice = computed(() =>{
  let msg = "";
  if(!isEmpty(suffix)){
     msg += "后缀【" + suffix + "】"
  }
  if(fileSize > 0){
    msg += "单文件【" + reckonFileSize(fileSize) + "】"
  }
  if(uploadFileNum > 0){
    msg += "数量【"  + uploadFileNum + "】"
  }
  if(!isEmpty(msg)){
    msg = "上传限制，" + msg;
  }
  return msg;
});

// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项，表示请求URL公共部分
  baseURL: import.meta.env.VITE_HTTP_BASE_URL,
  // 超时
  timeout: 1000000
});

const deleteUploadItem = (uuid: string) => {
  for (let i = 0; i < uploadFiles.length; i++) {
    if (uuid == uploadFiles[i].uuid) {
      uploadFiles[i].hasDelete = true;
      uploadFiles.splice(i, 1);
      queue.remove(uuid);
      break;
    }
  }
  lastSplitFileIndex--;
  currentBatchFiles -= 1;
}

const showFileUploadBtn = computed(() => {
  return choiceWay == ChoiceWay.FILE || choiceWay == ChoiceWay.FILE_DIR;
});

const showDIRUploadBtn = computed(() => {
  return choiceWay == ChoiceWay.DIR || choiceWay == ChoiceWay.FILE_DIR;
});

const closeCustomizeUpload = () => {
  beforeClose();
  concurrencyUploadState.value = false;
  if (commandCallBack) {
    commandCallBack();
  }
}

const choiceFiles = (event: any) => {
  let sizeError = "";
  let suffixError = "";
  let files = event.target.files;
  if (uploadFiles.length + files.length > uploadFileNum && uploadFileNum > 0) {
    ElMessage.error("上传文件超过设置的设置个数，【" + uploadFileNum + "】");
    return;
  }
  currentBatchFiles += files.length;
  for (let i = 0; i < files.length; i++) {
    const item = files[i];
    if (fileSize > 0 && item.size > fileSize) {
      sizeError += item.name + ";";
      continue;
    }
    if(!isEmpty(suffix) && !checkSuffix(item.name)){
      suffixError += item.name + ";";
      continue;
    }
    let uuid: string = getUUID();
    uploadFiles.push({
      index: -1,
      file: item,
      hasSplit: false,
      hasDelete: false,
      showDelete: true,
      uuid: uuid,
      reckonSize: reckonFileSize(item.size),
      uploadSize: 0,
      name: item.name,
      process: 0,
      uploadChunks: new Set()
    });
  }
  if (!isEmpty(sizeError)) {
    ElMessage.error("上传文件大小超过设置上限" + "【" + sizeError + "】");
  }
  if(!isEmpty(suffixError)){
    ElMessage.error("上传文件后缀不正确" + "【" + suffixError + "】");
  }
}

const checkSuffix = (fileName:string) => {
  for(const suffix of suffixArray){
    if(fileName.endsWith(suffix)){
      return true;
    }
  }
  return false;
}

const uploadFileQueue = () => {
  concurrencyUploadClosState.value = false;
  if (uploadFiles.length <= 0) {
    concurrencyUploadClosState.value = true;
    ElMessage.error("请选择文件");
    return;
  }
  if (lastSplitFileIndex == uploadFiles.length) {
    uploadFileFunc();
    return;
  }
  getChunk(uploadFiles);
}
const uploadFileFunc = () => {
  concurrencyUploadClosState.value = false;
  if (queue.size() <= 0) {
    ElMessage.info("请选择文件!");
    concurrencyUploadClosState.value = true;
    return;
  }
  concurrencyUploadNum = concurrencyUploadNum < queue.size() ? concurrencyUploadNum : queue.size();
  for (let i = 0; i < concurrencyUploadNum; i++) {
    uploadChunk(queue.take());
  }
}

function getChunk(uploadFiles: []) {
  const item: any = uploadFiles[lastSplitFileIndex];
  item.index = lastSplitFileIndex;
  createChunk(item.file, item.uuid).then((data) => {
    queue.putAll(<[]>data);
    lastSplitFileIndex++;
    if (lastSplitFileIndex == uploadFiles.length) {
      uploadFileFunc();
      return;
    }
    getChunk(uploadFiles);
  }, (error) => {
    concurrencyUploadClosState.value = true;
    ElMessage.error("上次失败，请检查网络后再试！")
  });
}

function createChunk(file: object, fileUUID: string) {
  return new Promise((resolve, reject) => {
    try {
      let result: any = [];
      let chunkCount = Math.ceil(file.size / splitSize);
      const tempWorkerCount = chunkCount < workerCount ? chunkCount : workerCount;
      const workerIndexs = Math.ceil(chunkCount / tempWorkerCount);
      let blob = new Blob([<BlobPart>file]);
      for (let i = 0; i < tempWorkerCount; i++) {

        let worker = new Worker(new URL('./Worker.js', import.meta.url));
        let startIndex = i * workerIndexs;
        let endIndex = (i + 1) * workerIndexs;

        if (endIndex > chunkCount) {
          endIndex = chunkCount;
        }
        worker.onmessage = (e) => {
          const data = e.data;//分片数据
          result.pushAll(data);
          if (result.length == chunkCount) {
            resolve(result);
          }
        };
        worker.postMessage({
          file,
          uuid: fileUUID,
          startIndex,
          endIndex,
          splitSize,
          chunkCount,
          blob: blob
        });
      }
    } catch (e) {
      reject(e);
    }
  });
}

function uploadChunk(item: any) {

  uploadChunkPromise(item).then((result: any) => {

    let resultVO: object = result[0];
    if (resultVO.code == -1) {
      queue.put(result[1]);
    }
    if (queue.size() <= 0) {
      return
    }
    uploadChunk(queue.take());
  }, (errorResult: any) => {
    ElMessage.error("上传失败，请检查网络!");
    concurrencyUploadClosState.value = true;
    error(UploadConstants.UPLOAD_PART_FILE_FAILED, errorResult);
  });
}

function mergeFilesAjax() {
  return new Promise((resolve, reject) => {
    service.post(mergeUrl, {"jsonStr": JSON.stringify(mergeFiles)}, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }).then(response => {
      resolve(response.data);
    })
        .catch(error => {
          reject(error);
        });
  });
}


function uploadChunkPromise(item: any) {
  return new Promise((resolve, reject) => {
        try {
          let formData = new FormData();
          for (const key in item) {
            formData.append(key, item[key]);
          }
          //processContentRef.value.scrollTop = item.index * 40;
          console.log(item.index * 40);
          service.post(url, formData, {
            headers: {
              'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: function (progressEvent: any) {
              if (progressEvent.loaded == progressEvent.total) {
                for (const uFile of uploadFiles) {
                  if (uFile.uuid == item.uuid) {
                    uFile.uploadChunks.add(item.index);
                    //uFile.uploadSize = reckonFileSize(uFile.uploadChunks.size * splitSize);
                    uFile.process = Math.round((uFile.uploadChunks.size * 100) / item.count);
                    if (uFile.uploadChunks.size == item.count) {
                      let mergeItem = {
                        'name': item.name,
                        'uuid': item.uuid,
                        'total': item.count
                      };
                      const targetElement = document.getElementById(`item-${uFile.index}`);
                      if (targetElement) {
                        targetElement.scrollIntoView({behavior: 'smooth'});
                      }
                      mergeFiles.push(mergeItem);
                      uFile.showDelete = false;
                      if (currentBatchFiles == mergeFiles.length) {
                        let ids = document.getElementsByClassName("customize-upload");
                        loadingInstance = ElLoading.service(
                            {
                              text: '数据合并中。。。',
                              target: ids[0]
                            });
                        mergeFilesAjax().then((data: any) => {
                          uploadSuccess(UploadConstants.SUCCESS, data.data);
                          currentBatchFiles = 0;
                          mergeFiles = [];
                          concurrencyUploadClosState.value = true;
                          loadingInstance.close();
                        }, (error: any) => {
                          loadingInstance.close();
                          console.log(error);
                          error(UploadConstants.MERGE_FILE_FAILED, error);
                        });
                      }
                    }
                    break;
                  }
                }
              }
              let percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            }
          }).then(response => {
            resolve([response.data, item]);
          }).catch(error => {
            queue.preAdd(item);
            reject(error);
          });
        } catch(e) {
          queue.preAdd(item);
          reject(e);
        }
      });
}

function getUUID() {
  let s: any = [];
  let hexDigits = "0123456789abcdef";
  for (let i = 0; i < 36; i++) {
    s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
  }
  s[14] = "4";
  s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
  s[8] = s[13] = s[18] = s[23] = "";

  let uuid = s.join("");
  return uuid;
}

///暴露方法
const show = (commandFunc: object) => {
  lastSplitFileIndex = 0;
  concurrencyUploadState.value = true;
  uploadFiles.splice(0, uploadFiles.length);
  queue.clear();
  mergeFiles.clear();
  currentBatchFiles = 0;
  commandCallBack = commandFunc;
}

const hide = () => {
  concurrencyUploadState.value = false;
}

const reckonFileSize = (douSize: number) => {
  const s = ["B", "KB", "M", "G"];
  let mark = true;
  let res = "";
  let i = 0;
  while (mark == true) {
    let temp = douSize / 1024;
    i += 1;
    if (temp < 1 || i > 3) {
      res = Math.floor(temp * 1024 * 100) / 100 + s[i - 1];
      mark = false;
    }
    douSize = temp;
  }
  return res;
}

defineExpose({show, hide});
</script>

<style >
.el-message ,.el-message--error {
  z-index: 999999999999 !important; /* 根据需要调整这个值 */
}
.customize-upload {
  width: 100vw;
  height: 100vh;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 9991;
}

.customize-upload-over {
  width: 100%;
  height: 100%;
  background-color: black;
  opacity: 0.5;
}

.customize-upload-content {
  width: 50%;
  height: 600px;
  background-color: white;
  position: absolute;
  left: 25%;
  top: calc(50% - 300px);
  padding-left: 10px;
  padding-right: 10px;
}

.customize-upload-content-head {
  width: 100%;
  height: 35px;
  padding-top: 15px;
}

.customize-upload-content-upload-btn {
  margin-top: 15px;
  padding: 8px 8px;
  color: white;
  border-radius: 5px;
}

.customize-upload-content-process {
  height: calc(100% - 60px);
  overflow: auto;
}

.customize-upload-content-process-item {
  width: 100%;
  height: 50px;
}

.show-process {
  width: 100%;
  height: 25px;
  position: relative;
}

.process-item-all {
  width: 100%;
  height: 100%;
  background-color: lightgray;
  border-radius: 5px;
}

.process-item-real {
  width: 20%;
  height: 100%;
  background-color: #2F61C7;
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 5px;
  color: white;
  text-align: right;
}

.show-info {
  width: 100%;
  height: 20px;
  display: flex;
  row-gap: 10px;
  font-size: 15px;
  color: #66686B;
}

.show-info-spacing {
  margin-right: 10px;
}

.customize-upload-close {
  float: right;
  font-size: 25px;
  margin-top: -15px;
  cursor: pointer;
}

.show-info-delete {
  font-size: 20px;
  margin-top: -2px;
  cursor: pointer;
}

.show-info-reckon-size {
  font-size: 15px;
}
</style>